package com.tobeface.wechat.message;

import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Ordering;
import com.tobeface.modules.lang.Langs;
import com.tobeface.modules.lang.Requires;
import com.tobeface.wechat.core.WeChatSettings;
import com.tobeface.wechat.core.timestamp.BadTimestampException;
import com.tobeface.wechat.core.timestamp.DefaultWeChatTimestampService;
import com.tobeface.wechat.core.timestamp.DuplicatedTimestampException;
import com.tobeface.wechat.core.timestamp.WeChatTimestampService;
import com.tobeface.wechat.message.handler.AppAccessMessageHandler;
import com.tobeface.wechat.message.handler.NOPMessageHandler;
import com.tobeface.wechat.message.tracing.DefaultWeChatMessageTracingService;
import com.tobeface.wechat.message.verifier.Sha1WeChatMessageVerifyService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author loudyn
 */
public class DefaultWeChatMessageDispatcher implements WeChatMessageDispatcher {

    private final WeChatSettings settings;
    private final List<WeChatMessageHandler> messageHandlers;

    private final Logger logger = LoggerFactory.getLogger(getClass());
    private final List<WeChatMessageHandler> builtInHandlers = ImmutableList.<WeChatMessageHandler>of(
            new AppAccessMessageHandler()
    );

    private WeChatMessageHandler defaultMessageHandler = new NOPMessageHandler();
    private WeChatTimestampService timestampService = new DefaultWeChatTimestampService();
    private WeChatMessageVerifyService messageVerifyService = new Sha1WeChatMessageVerifyService();
    private WeChatMessageTracingService messageTracingService = new DefaultWeChatMessageTracingService();

    /**
     * @param settings
     * @param messageHandlers
     */
    public DefaultWeChatMessageDispatcher(WeChatSettings settings,
                                          List<WeChatMessageHandler> messageHandlers) {

        Requires.notNull(settings, "Settings must not be null!");
        Requires.notNull(messageHandlers, "Handlers must not be empty!");

        this.settings = settings;
        this.messageHandlers = Ordering.natural().immutableSortedCopy(messageHandlers);
    }

    @Override
    public void dispatchMessage(WeChatMessageContext context) {

        Requires.notNull(context);

        WeChatMessageCredential credential = context.getCredential();
        String timestamp = credential.getTimestamp();
        if (!timestampService.isTimestampAcceptable(timestamp)) {
            throw new BadTimestampException(timestamp);
        }

        String nonce = credential.getNonce();
        // to prevent replay attack.
        if (timestampService.isTimestampNonceFound(timestamp, nonce)) {
            throw new DuplicatedTimestampException(timestamp, nonce);
        }
        timestampService.tracingTimestamp(timestamp, nonce);

        // verify request.
        if (!messageVerifyService.verify(settings, credential)) {
            throw new WeChatMessageVerifyException(credential);
        }

        WeChatMessage message = context.getMessage();

        // wechat server will retry when response time is too long.
        // the WeChatMessageHandler may be side effect.
        // we must try to remove duplicated messages.
        if (messageTracingService.isDuplicateMessage(message)) {
            ignoreDuplicatedMessage(context, message);
            return;
        }

        Stopwatch watch = new Stopwatch().start();
        try {

            messageTracingService.tracingMessage(message);
            dispatchMessageIntenal(context, message, messageHandlers, defaultMessageHandler);
        } catch (Exception e) {
            // give chance to rehandle.
            messageTracingService.untracingMessage(message);
            throw Langs.toUncheck(e);
        } finally {
            afterDispatchMessage(message, watch.stop().elapsed(TimeUnit.MILLISECONDS));
        }
    }

    private void ignoreDuplicatedMessage(WeChatMessageContext context, WeChatMessage message) {
        context.getChannel().write("");
        context.getChannel().flush();
    }

    protected void dispatchMessageIntenal(WeChatMessageContext context, WeChatMessage message,
                                          List<WeChatMessageHandler> messageHandlers, WeChatMessageHandler defaultMessageHandler) {

        // find user define first.
        for (WeChatMessageHandler handler : messageHandlers) {
            if (!handler.isSupport(message)) {
                continue;
            }

            handler.messageReceived(context, message);
            return;
        }

        // find builtins message handler.
        for (WeChatMessageHandler builtin : builtInHandlers) {
            if (!builtin.isSupport(message)) {
                continue;
            }

            getLogger().info("WeChatMessage handle by builtInHandlers.");
            builtin.messageReceived(context, message);
            return;
        }

        // now, try defaultMessageHandler.
        if (defaultMessageHandler.isSupport(message)) {
            getLogger().info("WeChatMessage handle by defaultMessageHandler.");
            defaultMessageHandler.messageReceived(context, message);
            return;
        }

        throw new WeChatMessageHandlerNotFoundException();
    }


    /**
     * @param message
     * @param elapsed
     */
    protected void afterDispatchMessage(WeChatMessage message, long elapsed) {
    }

    /**
     * @return
     */
    protected Logger getLogger() {
        return logger;
    }

    /**
     * @param defaultMessageHandler
     */
    public void setDefaultMessageHandler(WeChatMessageHandler defaultMessageHandler) {
        Requires.notNull(defaultMessageHandler, "DefaultMessageHandler must not be null!");
        this.defaultMessageHandler = defaultMessageHandler;
    }

    /**
     * @param timestampService
     */
    public void setTimestampService(WeChatTimestampService timestampService) {
        Requires.notNull(timestampService, "TimestampSerive must not be null!");
        this.timestampService = timestampService;
    }

    /**
     * @param messageTracingService
     */
    public void setMessageTracingService(WeChatMessageTracingService messageTracingService) {
        Requires.notNull(messageTracingService, "MessageTracingService must not be null!");
        this.messageTracingService = messageTracingService;
    }

    /**
     * @param messageVerifyService
     */
    public void setMessageVerifyService(WeChatMessageVerifyService messageVerifyService) {
        Requires.notNull(messageTracingService, "MessageVerifier must not be null!");
        this.messageVerifyService = messageVerifyService;
    }
}
